// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.server.restapi.change;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
import static com.google.gerrit.server.permissions.ChangePermission.REVERT;
import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
import static java.util.Objects.requireNonNull;

import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.RevertSubmissionInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.WalkSorter;
import com.google.gerrit.server.change.WalkSorter.PatchSetData;
import com.google.gerrit.server.git.CommitUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ContributorAgreementsChecker;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.restapi.change.CherryPickChange.Result;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.CommitMessageUtil;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;

public class RevertSubmission
    implements RestModifyView<ChangeResource, RevertInput>, UiAction<ChangeResource> {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  private final Provider<InternalChangeQuery> queryProvider;
  private final Provider<CurrentUser> user;
  private final PermissionBackend permissionBackend;
  private final ProjectCache projectCache;
  private final PatchSetUtil psUtil;
  private final ContributorAgreementsChecker contributorAgreements;
  private final CherryPickChange cherryPickChange;
  private final ChangeJson.Factory json;
  private final GitRepositoryManager repoManager;
  private final WalkSorter sorter;
  private final CommitUtil commitUtil;
  private final ChangeNotes.Factory changeNotesFactory;
  private final Sequences seq;
  private final NotifyResolver notifyResolver;
  private final BatchUpdate.Factory updateFactory;
  private final ChangeResource.Factory changeResourceFactory;
  private final GetRelated getRelated;

  private CherryPickInput cherryPickInput;
  private List<ChangeInfo> results;
  private static final Pattern patternRevertSubject = Pattern.compile("Revert \"(.+)\"");
  private static final Pattern patternRevertSubjectWithNum =
      Pattern.compile("Revert\\^(\\d+) \"(.+)\"");

  @Inject
  RevertSubmission(
      Provider<InternalChangeQuery> queryProvider,
      Provider<CurrentUser> user,
      PermissionBackend permissionBackend,
      ProjectCache projectCache,
      PatchSetUtil psUtil,
      ContributorAgreementsChecker contributorAgreements,
      CherryPickChange cherryPickChange,
      ChangeJson.Factory json,
      GitRepositoryManager repoManager,
      WalkSorter sorter,
      CommitUtil commitUtil,
      ChangeNotes.Factory changeNotesFactory,
      Sequences seq,
      NotifyResolver notifyResolver,
      BatchUpdate.Factory updateFactory,
      ChangeResource.Factory changeResourceFactory,
      GetRelated getRelated) {
    this.queryProvider = queryProvider;
    this.user = user;
    this.permissionBackend = permissionBackend;
    this.projectCache = projectCache;
    this.psUtil = psUtil;
    this.contributorAgreements = contributorAgreements;
    this.cherryPickChange = cherryPickChange;
    this.json = json;
    this.repoManager = repoManager;
    this.sorter = sorter;
    this.commitUtil = commitUtil;
    this.changeNotesFactory = changeNotesFactory;
    this.seq = seq;
    this.notifyResolver = notifyResolver;
    this.updateFactory = updateFactory;
    this.changeResourceFactory = changeResourceFactory;
    this.getRelated = getRelated;
    results = new ArrayList<>();
    cherryPickInput = null;
  }

  @Override
  public Response<RevertSubmissionInfo> apply(ChangeResource changeResource, RevertInput input)
      throws RestApiException,
          IOException,
          UpdateException,
          PermissionBackendException,
          NoSuchProjectException,
          ConfigInvalidException,
          StorageException {

    if (!changeResource.getChange().isMerged()) {
      throw new ResourceConflictException(
          String.format("change is %s.", ChangeUtil.status(changeResource.getChange())));
    }

    String submissionId = changeResource.getChange().getSubmissionId();
    if (submissionId == null) {
      throw new ResourceConflictException(
          "This change is merged but doesn't have a submission id,"
              + " meaning it was not submitted through Gerrit.");
    }
    List<ChangeData> changeDatas = queryProvider.get().bySubmissionId(submissionId);

    checkPermissionsForAllChanges(changeResource, changeDatas);
    input.topic = createTopic(input.topic, submissionId);

    return Response.ok(revertSubmission(changeDatas, input));
  }

  private String createTopic(String topic, String submissionId) {
    if (topic != null) {
      topic = Strings.emptyToNull(topic.trim());
    }
    if (topic == null) {
      return String.format(
          "revert-%s-%s",
          submissionId, RandomStringUtils.randomAlphabetic(10).toUpperCase(Locale.US));
    }
    return topic;
  }

  private void checkPermissionsForAllChanges(
      ChangeResource changeResource, List<ChangeData> changeDatas)
      throws IOException, AuthException, PermissionBackendException, ResourceConflictException {
    for (ChangeData changeData : changeDatas) {
      Change change = changeData.change();

      // Might do the permission tests multiple times, but these are necessary to ensure that the
      // user has permissions to revert all changes. If they lack any permission, no revert will be
      // done.

      contributorAgreements.check(change.getProject(), changeResource.getUser());
      permissionBackend.currentUser().ref(change.getDest()).check(CREATE_CHANGE);
      permissionBackend.currentUser().change(changeData).check(REVERT);
      permissionBackend.currentUser().change(changeData).check(ChangePermission.READ);
      projectCache
          .get(change.getProject())
          .orElseThrow(illegalState(change.getProject()))
          .checkStatePermitsWrite();

      requireNonNull(
          psUtil.get(changeData.notes(), change.currentPatchSetId()),
          String.format(
              "current patch set %s of change %s not found",
              change.currentPatchSetId(), change.currentPatchSetId()));
    }
  }

  private RevertSubmissionInfo revertSubmission(
      List<ChangeData> changeData, RevertInput revertInput)
      throws RestApiException,
          IOException,
          UpdateException,
          ConfigInvalidException,
          StorageException,
          PermissionBackendException {

    ListMultimap<BranchNameKey, ChangeData> changesPerProjectAndBranch = ArrayListMultimap.create();
    changeData.stream().forEach(c -> changesPerProjectAndBranch.put(c.change().getDest(), c));
    cherryPickInput = createCherryPickInput(revertInput);
    Instant timestamp = TimeUtil.now();

    String initialMessage = revertInput.message;
    for (BranchNameKey projectAndBranch : changesPerProjectAndBranch.keySet()) {
      cherryPickInput.base = null;
      Project.NameKey project = projectAndBranch.project();
      cherryPickInput.destination = projectAndBranch.branch();
      List<ChangeData> changesInProjectAndBranch = changesPerProjectAndBranch.get(projectAndBranch);

      // Sort the changes topologically.
      Iterator<PatchSetData> sortedChangesInProjectAndBranch =
          sorter.sort(changesInProjectAndBranch).iterator();

      Set<ObjectId> commitIdsInProjectAndBranch =
          changesInProjectAndBranch.stream()
              .map(c -> c.currentPatchSet().commitId())
              .collect(Collectors.toSet());

      revertAllChangesInProjectAndBranch(
          initialMessage,
          revertInput,
          project,
          sortedChangesInProjectAndBranch,
          commitIdsInProjectAndBranch,
          timestamp);
    }
    results.sort(Comparator.comparing(c -> c.revertOf));
    RevertSubmissionInfo revertSubmissionInfo = new RevertSubmissionInfo();
    revertSubmissionInfo.revertChanges = results;
    return revertSubmissionInfo;
  }

  // Warning: reuses and modifies revertInput.message.
  private void revertAllChangesInProjectAndBranch(
      String initialMessage,
      RevertInput revertInput,
      Project.NameKey project,
      Iterator<PatchSetData> sortedChangesInProjectAndBranch,
      Set<ObjectId> commitIdsInProjectAndBranch,
      Instant timestamp)
      throws IOException,
          RestApiException,
          UpdateException,
          ConfigInvalidException,
          PermissionBackendException {
    while (sortedChangesInProjectAndBranch.hasNext()) {
      ChangeNotes changeNotes = sortedChangesInProjectAndBranch.next().data().notes();
      if (cherryPickInput.base == null) {
        // If no base was provided, the first change will be used to find a common base.
        cherryPickInput.base = getBase(changeNotes, commitIdsInProjectAndBranch).name();
      }

      // Set revert message for the current revert change.
      revertInput.message = getMessage(initialMessage, changeNotes);
      if (cherryPickInput.base.equals(changeNotes.getCurrentPatchSet().commitId().getName())) {
        // This is the code in case this is the first revert of this project + branch, and the
        // revert would be on top of the change being reverted.
        createNormalRevert(revertInput, changeNotes, timestamp);
      } else {
        createCherryPickedRevert(revertInput, project, changeNotes, timestamp);
      }
    }
  }

  private void createCherryPickedRevert(
      RevertInput revertInput, Project.NameKey project, ChangeNotes changeNotes, Instant timestamp)
      throws IOException, ConfigInvalidException, UpdateException, RestApiException {
    ObjectId revCommitId =
        commitUtil.createRevertCommit(revertInput.message, changeNotes, user.get(), timestamp);
    // TODO (paiking): As a future change, the revert should just be done directly on the
    // target rather than just creating a commit and then cherry-picking it.
    cherryPickInput.message = revertInput.message;
    ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
    Change.Id cherryPickRevertChangeId = Change.id(seq.nextChangeId());
    RevCommit baseCommit = null;
    if (cherryPickInput.base != null) {
      try (Repository git = repoManager.openRepository(changeNotes.getProjectName());
          RevWalk revWalk = new RevWalk(git.newObjectReader())) {
        baseCommit = revWalk.parseCommit(ObjectId.fromString(cherryPickInput.base));
      }
    }
    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
      try (BatchUpdate bu = updateFactory.create(project, user.get(), TimeUtil.now())) {
        bu.setNotify(
            notifyResolver.resolve(
                firstNonNull(cherryPickInput.notify, NotifyHandling.ALL),
                cherryPickInput.notifyDetails));
        bu.addOp(
            changeNotes.getChange().getId(),
            new CreateCherryPickOp(
                revCommitId,
                generatedChangeId,
                cherryPickRevertChangeId,
                timestamp,
                revertInput.getWorkInProgress(),
                baseCommit));
        if (!revertInput.getWorkInProgress()) {
          commitUtil.addChangeRevertedNotificationOps(
              bu, changeNotes.getChangeId(), cherryPickRevertChangeId, generatedChangeId.name());
        }
        bu.execute();
      }
    }
  }

  private void createNormalRevert(
      RevertInput revertInput, ChangeNotes changeNotes, Instant timestamp)
      throws IOException, RestApiException, UpdateException, ConfigInvalidException {

    Change.Id revertId =
        commitUtil.createRevertChange(changeNotes, user.get(), revertInput, timestamp);
    results.add(json.noOptions().format(changeNotes.getProjectName(), revertId));
    cherryPickInput.base =
        changeNotesFactory
            .createChecked(changeNotes.getProjectName(), revertId)
            .getCurrentPatchSet()
            .commitId()
            .getName();
  }

  private CherryPickInput createCherryPickInput(RevertInput revertInput) {
    cherryPickInput = new CherryPickInput();
    // To create a revert change, we create a revert commit that is then cherry-picked. The revert
    // change is created for the cherry-picked commit. Notifications are sent only for this change,
    // but not for the intermediately created revert commit.
    cherryPickInput.notify = revertInput.notify;
    if (revertInput.getWorkInProgress()) {
      cherryPickInput.notify = firstNonNull(cherryPickInput.notify, NotifyHandling.NONE);
    }
    cherryPickInput.notifyDetails = revertInput.notifyDetails;
    cherryPickInput.parent = 1;
    cherryPickInput.keepReviewers = true;
    cherryPickInput.topic = revertInput.topic;
    cherryPickInput.allowEmpty = true;
    return cherryPickInput;
  }

  private String getMessage(String initialMessage, ChangeNotes changeNotes) {
    String subject = changeNotes.getChange().getSubject();
    if (subject.length() > 60) {
      subject = subject.substring(0, 56) + "...";
    }
    if (initialMessage == null) {
      initialMessage =
          MessageFormat.format(
              ChangeMessages.revertSubmissionDefaultMessage,
              changeNotes.getCurrentPatchSet().commitId().name());
    }

    // For performance purposes: Almost all cases will end here.
    if (!subject.startsWith("Revert")) {
      return MessageFormat.format(
          ChangeMessages.revertSubmissionUserMessage, subject, initialMessage);
    }

    Matcher matcher = patternRevertSubjectWithNum.matcher(subject);

    if (matcher.matches()) {
      return MessageFormat.format(
          ChangeMessages.revertSubmissionOfRevertSubmissionUserMessage,
          Integer.valueOf(matcher.group(1)) + 1,
          matcher.group(2),
          changeNotes.getCurrentPatchSet().commitId().name());
    }

    matcher = patternRevertSubject.matcher(subject);
    if (matcher.matches()) {
      return MessageFormat.format(
          ChangeMessages.revertSubmissionOfRevertSubmissionUserMessage,
          2,
          matcher.group(1),
          changeNotes.getCurrentPatchSet().commitId().name());
    }

    return MessageFormat.format(
        ChangeMessages.revertSubmissionUserMessage, subject, initialMessage);
  }

  /**
   * This function finds the base that the first revert in a project + branch should be based on.
   *
   * <p>If there is only one change, we will base the revert on that change. If all changes are
   * related, we will base on the first commit of this submission in the topological order.
   *
   * <p>If none of those special cases applies, the only case left is the case where we have at
   * least 2 independent changes in the same project + branch (and possibly other dependent
   * changes). In this case, it searches using BFS for the first commit that is either: 1. Has 2 or
   * more parents, and has as parents at least one commit that is part of the submission. 2. A
   * commit that is part of the submission. If neither of those are true, it just continues the
   * search by going to the parents.
   *
   * <p>If 1 is true, it means that this merge commit was created when this submission was
   * submitted. It also means that this merge commit is a descendant of all of the changes in this
   * submission and project + branch. Therefore, we return this merge commit.
   *
   * <p>If 2 is true, it will return the commit that WalkSorter has decided that it should be the
   * first commit reverted (e.g changeNotes, which is also the commit that is the first in the
   * topological sorting).
   *
   * <p>It doesn't run through the entire graph since it will stop once it finds at least one commit
   * that is part of the submission.
   *
   * @param changeNotes changeNotes for the change that is found by WalkSorter to be the first one
   *     that should be reverted, the first in the topological sorting.
   * @param commitIds The commitIds of this project and branch.
   * @return the base of the first revert.
   */
  private ObjectId getBase(ChangeNotes changeNotes, Set<ObjectId> commitIds)
      throws StorageException, IOException, PermissionBackendException {
    // If there is only one change in that project and branch, just base the revert on that one
    // change.
    if (commitIds.size() == 1) {
      return Iterables.getOnlyElement(commitIds);
    }
    // If all changes are related, just return the first commit of this submission in the
    // topological sorting.
    if (getRelated.getRelated(getRevisionResource(changeNotes)).stream()
        .map(changes -> ObjectId.fromString(changes.commit.commit))
        .collect(Collectors.toSet())
        .containsAll(commitIds)) {
      return changeNotes.getCurrentPatchSet().commitId();
    }
    // There are independent changes in this submission and repository + branch.
    try (Repository git = repoManager.openRepository(changeNotes.getProjectName());
        ObjectInserter oi = git.newObjectInserter();
        ObjectReader reader = oi.newReader();
        RevWalk revWalk = new RevWalk(reader)) {

      ObjectId startCommit =
          git.getRefDatabase().findRef(changeNotes.getChange().getDest().branch()).getObjectId();
      revWalk.markStart(revWalk.parseCommit(startCommit));
      markChangesParentsUninteresting(commitIds, revWalk);
      Iterator<RevCommit> revWalkIterator = revWalk.iterator();
      while (revWalkIterator.hasNext()) {
        RevCommit revCommit = revWalkIterator.next();
        if (commitIds.contains(revCommit.getId())) {
          return changeNotes.getCurrentPatchSet().commitId();
        }
        if (Arrays.stream(revCommit.getParents())
            .anyMatch(parent -> commitIds.contains(parent.getId()))) {
          // Found a merge commit that at least one parent is in this submission. we should only
          // reach here if both conditions apply:
          // 1. There is more than one change in that project + branch in this submission.
          // 2. Not all changes in that project + branch are related in this submission.
          // Therefore, there are at least 2 unrelated changes in this project + branch that got
          // submitted together,
          // and since we found a merge commit with one of those as parents, this merge commit is
          // the first common descendant of all those changes.
          return revCommit.getId();
        }
      }
      // This should never happen since it can only happen if we go through the entire repository
      // without finding a single commit that matches any commit from the submission.
      throw new StorageException(
          String.format(
              "Couldn't find change %s in the repository %s",
              changeNotes.getChangeId(), changeNotes.getProjectName().get()));
    }
  }

  private RevisionResource getRevisionResource(ChangeNotes changeNotes) {
    return new RevisionResource(
        changeResourceFactory.create(changeNotes, user.get()), psUtil.current(changeNotes));
  }

  // The parents are not interesting since there is no reason to base the reverts on any of the
  // parents or their ancestors.
  private void markChangesParentsUninteresting(Set<ObjectId> commitIds, RevWalk revWalk)
      throws IOException {
    for (ObjectId id : commitIds) {
      RevCommit revCommit = revWalk.parseCommit(id);
      for (int i = 0; i < revCommit.getParentCount(); i++) {
        revWalk.markUninteresting(revCommit.getParent(i));
      }
    }
  }

  @Override
  public Description getDescription(ChangeResource rsrc) {
    Change change = rsrc.getChange();
    boolean projectStatePermitsWrite = false;
    try {
      projectStatePermitsWrite =
          projectCache.get(rsrc.getProject()).map(ProjectState::statePermitsWrite).orElse(false);
    } catch (StorageException e) {
      logger.atSevere().withCause(e).log(
          "Failed to check if project state permits write: %s", rsrc.getProject());
    }
    return new UiAction.Description()
        .setLabel("Revert submission")
        .setTitle(
            "Revert this change and all changes that have been submitted together with this change")
        .setVisible(
            and(
                and(
                    change.isMerged()
                        && change.getSubmissionId() != null
                        && isChangePartOfSubmission(change.getSubmissionId())
                        && projectStatePermitsWrite,
                    permissionBackend
                        .user(rsrc.getUser())
                        .ref(change.getDest())
                        .testCond(CREATE_CHANGE)),
                permissionBackend.user(rsrc.getUser()).change(rsrc.getNotes()).testCond(REVERT)));
  }

  /**
   * @param submissionId the submission id of the change.
   * @return True if the submission has more than one change, false otherwise.
   */
  private Boolean isChangePartOfSubmission(String submissionId) {
    return (queryProvider.get().setLimit(2).bySubmissionId(submissionId).size() > 1);
  }

  private class CreateCherryPickOp implements BatchUpdateOp {
    private final ObjectId revCommitId;
    private final ObjectId computedChangeId;
    private final Change.Id cherryPickRevertChangeId;
    private final Instant timestamp;
    private final boolean workInProgress;
    private final RevCommit baseCommit;

    CreateCherryPickOp(
        ObjectId revCommitId,
        ObjectId computedChangeId,
        Change.Id cherryPickRevertChangeId,
        Instant timestamp,
        Boolean workInProgress,
        RevCommit baseCommit) {
      this.revCommitId = revCommitId;
      this.computedChangeId = computedChangeId;
      this.cherryPickRevertChangeId = cherryPickRevertChangeId;
      this.timestamp = timestamp;
      this.workInProgress = workInProgress;
      this.baseCommit = baseCommit;
    }

    @Override
    public boolean updateChange(ChangeContext ctx) throws Exception {
      Change change = ctx.getChange();
      Result cherryPickResult =
          cherryPickChange.cherryPick(
              change,
              change.currentPatchSetId(),
              change.getProject(),
              revCommitId,
              cherryPickInput,
              BranchNameKey.create(
                  change.getProject(), RefNames.fullName(cherryPickInput.destination)),
              timestamp,
              change.getId(),
              computedChangeId,
              cherryPickRevertChangeId,
              workInProgress,
              Optional.ofNullable(baseCommit));
      // save the commit as base for next cherryPick of that branch
      cherryPickInput.base =
          changeNotesFactory
              .createChecked(ctx.getProject(), cherryPickResult.changeId())
              .getCurrentPatchSet()
              .commitId()
              .getName();
      results.add(json.noOptions().format(change.getProject(), cherryPickResult.changeId()));
      return true;
    }
  }
}
